/*
 * Copyright (C) 2008 Peter Kling
 *
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 *
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * 'A Kind of Billiard'. If not, see <http://www.gnu.org/licenses/>.
 */

package cg2.models;

import java.util.ArrayList;

import javax.media.opengl.GL;

import cg2.misc.OpenGLDrawable;

/**
 * @brief Class for complex OpenGL objects consisting of many primitives and different materials.
 */
public class GLModel implements OpenGLDrawable {
	// object data
	private ArrayList<double[]> vertices;
	private ArrayList<double[]> texels;
	private ArrayList<double[]> normals;
	
	// data for faces, materials, ...
	private ArrayList<ArrayList<int[]>> primitiveClusters_vertices;
	private ArrayList<ArrayList<int[]>> primitiveClusters_texels;
	private ArrayList<ArrayList<int[]>> primitiveClusters_normals;
	private ArrayList<GLMaterial>       primitiveClusters_materials;
	
	// display list for object
	private int displayList;
	
	// whether this this model supports shading (or whether it is requested)
	private boolean shadable           = true;
	private boolean shadable_requested = true;
	
	/**
	 * @brief Constructs an GLObject using the given vertices, texels, normals and primitive clusters.
	 * 
	 * Primitive clusters consists of several arraylists that carry data for a bunch of polygons (faces, texels, ...).
	 * The vertices, texels and normals primitive clusters are integer array that reference to indices for the vertices,
	 * texels and normals arrays. 
	 */
	public GLModel(ArrayList<double[]> vertices, ArrayList<double[]> texels, ArrayList<double[]> normals,
			ArrayList<ArrayList<int[]>> pc_vertices, ArrayList<ArrayList<int[]>> pc_texels,
			ArrayList<ArrayList<int[]>> pc_normals,  ArrayList<GLMaterial> pc_materials) {
		this.vertices = vertices;
		this.texels   = texels;
		this.normals  = normals;
		this.primitiveClusters_vertices  = pc_vertices;
		this.primitiveClusters_texels    = pc_texels;
		this.primitiveClusters_normals   = pc_normals;
		this.primitiveClusters_materials = pc_materials;
	}
	
	/**
	 * @brief Draws this object.
	 * 
	 * If the display list of this object is not yet updated this method will call updateDisplayList() prior to drawing
	 * the object.
	 */
	public void draw(GL gl) {
		if (displayList == 0 || shadable != shadable_requested)
			updateDisplayList(gl);
		gl.glCallList(displayList);
	}
	
	/**
	 * @brief Returns the current display list of the model.
	 */
	public int getDisplayList() {
		return displayList;
	}
	
	/**
	 * @brief Returns whether this object is shadable (using volume shaders).
	 * 
	 * Objects supporting volume shaders have to provide additional vertices.
	 * 
	 * @see renderEdgeQuadriliterals
	 */
	public boolean isShadable() {
		return shadable;
	}
	
	/**
	 * @brief Will render every edge in the given vertex data as a quadriliteral (GL_QUADS).
	 * 
	 * If you want to use shading by volume shadows (using the shadow volume shader) with a model, it has to provide
	 * additional 'zero-width' faces (namely every edge has to be transformed into a face). This is due to the way
	 * the shadow volume is created by the shader). This method will render these edges (you should use it to create
	 * a display list). 
	 */
	private void renderEdgeQuadriliterals(GL gl) {
		// create single array of all faces (easier to access; primitive clusters are of no importance here)
		ArrayList<int[]> faces = new ArrayList<int[]>();
		ArrayList<int[]> normalFaces = new ArrayList<int[]>();
		for (int i=0; i<primitiveClusters_vertices.size(); i++) {
			ArrayList<int[]> faces_v  = primitiveClusters_vertices.get(i);
			ArrayList<int[]> faces_n  = primitiveClusters_normals.get(i);
			for (int j=0; j<faces_v.size(); j++) {
				faces.add(faces_v.get(j));
				normalFaces.add(faces_n.get(j));
			}
		}
		
		// draw quadriliterals for every edge
		gl.glBegin(GL.GL_QUADS);
		for (int i=0; i<faces.size(); i++) {
			for (int j=i+1; j<faces.size(); j++) {
				int[] face1 = faces.get(i);
				int[] face2 = faces.get(j);
				int[] nFace1 = normalFaces.get(i);
				int[] nFace2 = normalFaces.get(j);
				for (int k=0; k<face1.length; k++) {
					for (int l=0; l<face2.length; l++) {
						int idx11 = k, idx21 = l;
						int idx12 = (k+1) % face1.length, idx22 = (l+1) % face2.length;
						if (face1[idx11] == face2[idx22] && face1[idx12] == face2[idx21]) {						
							gl.glNormal3dv(normals.get(nFace2[idx22]),0); gl.glVertex4dv(vertices.get(face2[idx22]),0);
							gl.glNormal3dv(normals.get(nFace2[idx21]),0); gl.glVertex4dv(vertices.get(face2[idx21]),0);
							gl.glNormal3dv(normals.get(nFace1[idx12]),0); gl.glVertex4dv(vertices.get(face1[idx12]),0);
							gl.glNormal3dv(normals.get(nFace1[idx11]),0); gl.glVertex4dv(vertices.get(face1[idx11]),0);
						}
					}
				}
			}
		}
		gl.glEnd();
	}
	
	/**
	 * @brief Sets whether this model can be used for shadow volumes.
	 * 
	 * Models need additional vertices for use of shadow volumes. Be aware that this model will cause a large delay as
	 * soon as the model is redrawn (but only once).
	 * 
	 * @see renderEdgeQuadriliterals
	 */
	public void setShadable(boolean value) {
		this.shadable_requested = value;
	}
	
	/**
	 * @brief Updates the display list of the object according to its current data.
	 */
	public void updateDisplayList(GL gl) {
		// create and start new display list
		displayList = gl.glGenLists(1);
		gl.glNewList(displayList, GL.GL_COMPILE);
		
		// save some parts of the OpenGL state
		gl.glPushAttrib(GL.GL_TEXTURE_BIT);
		
		// render the different primitive clusters
		for (int i=0; i<primitiveClusters_vertices.size(); i++) {
			ArrayList<int[]> faces_v  = primitiveClusters_vertices.get(i);
			ArrayList<int[]> faces_t  = primitiveClusters_texels.get(i);
			ArrayList<int[]> faces_n  = primitiveClusters_normals.get(i);
			GLMaterial       material = primitiveClusters_materials.get(i);
			
			if (faces_v.size() == 0)
				continue;
			int PRIMITIVE_TYPE;
			if (faces_v.get(0).length == 3)
				PRIMITIVE_TYPE = GL.GL_TRIANGLES;
			else if (faces_v.get(0).length == 4)
				PRIMITIVE_TYPE = GL.GL_QUADS;
			else
				PRIMITIVE_TYPE = GL.GL_POLYGON;
			
			// load material of current primitive cluster
			if (material != null)
				material.enable(gl);

			// draw the current primitive cluster
			if (PRIMITIVE_TYPE != GL.GL_POLYGON)
				gl.glBegin(PRIMITIVE_TYPE);
			for (int j=0; j<faces_v.size(); j++) {
				int[] face_v = faces_v.get(j);
				int[] face_t = (faces_t == null) ? null : faces_t.get(j);
				int[] face_n = (faces_n == null) ? null : faces_n.get(j);
				
				// draw face
				if (PRIMITIVE_TYPE == GL.GL_POLYGON)
					gl.glBegin(PRIMITIVE_TYPE);
				for (int k=0; k<face_v.length; k++) {
					if (face_n != null) gl.glNormal3dv(normals.get(face_n[k]), 0);
					if (face_t != null) gl.glTexCoord3dv(texels.get(face_t[k]), 0);
					gl.glVertex4dv(vertices.get(face_v[k]), 0);
				}
				if (PRIMITIVE_TYPE == GL.GL_POLYGON)
					gl.glEnd();
			}
			if (PRIMITIVE_TYPE != GL.GL_POLYGON)
				gl.glEnd();
		}
		
		// if requested, draw extra edges for shading by use of volume shadows
		if (isShadable())
			renderEdgeQuadriliterals(gl);
		
		// restore saved OpenGL state parameters
		gl.glPopAttrib();
		
		// end display list
		gl.glEndList();
	}
}
